﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace AMEE.TcpComm
{
    /// <summary>
    /// Argument of a ResponseReceived event.
    /// </summary>
    public class ResponseReceivedArg : EventArgs
    {
        /// <summary>
        /// Gets or sets the response.
        /// </summary>
        /// <value>
        /// The response.
        /// </value>
        public String Response { get; set; }
    }


    /// <summary>
    /// Offers Send messages over Tcp.
    /// </summary>
    public class TcpWriter
    {
        #region Fields

        TcpClient _client;
        Thread _readerThr;
        static readonly int _bufferSize = 2048;
        private bool _doRead;

        #endregion


        #region Events & Properties

        /// <summary>
        /// Occurs when received a response.
        /// </summary>
        public event EventHandler<ResponseReceivedArg> ResponseReceived;

        /// <summary>
        /// Occurs when message was sent.
        /// </summary>
        public event EventHandler MessageSent;
        

        /// <summary>
        /// Gets a value indicating whether this <see cref="TcpWriter"/> is connected.
        /// </summary>
        /// <value>
        ///   <c>true</c> if connected; otherwise, <c>false</c>.
        /// </value>
        public bool Connected {
            get {
                if (_client == null)
                    return false;
                else
                    return _client.Connected;
            }
       }

        /// <summary>
        /// Gets or sets the IP to Connect to.
        /// </summary>
        /// <value>
        /// The IP.
        /// </value>
        public String IP { get; set; }

        /// <summary>
        /// Gets or sets the port to connect to.
        /// </summary>
        /// <value>
        /// The port.
        /// </value>
        public int Port { get; set; }

        /// <summary>
        /// Gets the ID.
        /// </summary>
        public String ID { get; private set; }

        /// <summary>
        /// Gets or sets the encoding of messages sent and received. Defaukt is ASCII
        /// </summary>
        /// <value>
        /// The encoding.
        /// </value>
        public Encoding Encoding { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether the connection automatically will be closes after send (and receive).<para>
        /// Default is False</para>
        /// </summary>
        /// <value>
        ///   <c>true</c> if [close after sent]; otherwise, <c>false</c>.
        /// </value>
        public bool CloseAfterSent { get; set; }

        #endregion


        #region constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="TcpWriter"/> class and connects to the specified server.
        /// </summary>
        /// <param name="ip">The ip.</param>
        /// <param name="port">The port.</param>
        /// <remarks>
        /// Setzt die Properties und baut eine TCP Verbindung auf. Falls diese fehlschlägt, ist Connected = false; ansonsten, bei Erfolg auf true
        /// </remarks>
        public TcpWriter(String ip, int port) {
            this.ID = ip.Substring(ip.LastIndexOf('.')) + "/" + port;
            IP = ip;
            this.Encoding = Encoding.ASCII;
            this.Port = port;
            try {
                this.Connect();
            }
            catch (Exception) {
                Debug.WriteLine("TcpWriter init failure. Could not connect to " + ip + ":" + port, "Warning");
            }
        }

        #endregion


        #region Public

        /// <summary>
        /// Connects to the IP and Port, specified by the instance' properties.
        /// </summary>
        /// <remarks>
        /// When not IsConnected, you can optionally set new IP and Port and / or retry a connection with this method.
        /// </remarks>
        public void Connect() {
            if (Connected) {
                return;
            }
            else {
                _doRead = false;
                if (_readerThr != null && _readerThr.IsAlive) {
                    _readerThr.Join(3000);
                }
                if (_client != null) {
                    _client.Close();
                }
                try {
                    _client = new TcpClient(IP, Port);
                    _readerThr = new Thread(() => reader());
                    _readerThr.Name = "NetStreamReaderThread(" + IP + ":" + Port + ")-"+DateTime.Now.Millisecond;
                    _doRead = true;
                    _readerThr.Start();
                }
                catch (Exception) {
                    Debug.WriteLine("TcpWriter connection failure. Could not connect to " + IP + ":" + Port, "Warning");
                }
            }
        }

        /// <summary>
        /// Closes the Tcp connection.
        /// </summary>
        public void Close() {
            if (Connected) {
                Task.Run(() => {
                    _doRead = false;
                    _readerThr.Join(3000);
                    _client.Close();
                });
            }
        }

        /// <summary>
        /// Sends the specified message.<para>
        /// Blocks until a response received.</para>
        /// </summary>
        /// <param name="message">The message.</param>
        /// <returns></returns>
        public String SendBlocking(String message) {
            if (!Connected) {
                Debug.WriteLine("Sender.Write - client not connected!", "Warning");
                return null;
            }
            try {
                NetworkStream netStrm = _client.GetStream();
                // Translate the passed message into ASCII and store it as a Byte array.
                Byte[] data = Encoding.GetBytes(message);
                // Send the message to the connected TcpServer.

                netStrm.Write(data, 0, data.Length);

                Debug.WriteLine("Sent: " + message);

                // Receive the TcpServer.response. 

                // Buffer to store the response bytes.
                data = new Byte[_bufferSize];

                // String to store the response ASCII representation.
                String responseData = "";

                // Read the first batch of the TcpServer response bytes.
                do {
                    int bytes = netStrm.Read(data, 0, data.Length);
                    responseData += Encoding.GetString(data, 0, bytes);
                } while (netStrm.DataAvailable);
                Debug.WriteLine("Received: " + responseData);
                return responseData;
            }
            catch (Exception e) {
                Debug.WriteLine("SendBlocking(msg) Error - " + e.Message, "Warning");
                this.Close();
                return null;
            }
            finally {
                if (CloseAfterSent)
                    this.Close();
            }
        }
        

        /// <summary>
        /// Sends the specified message.<para>
        /// Throws MessageSent and a ResponseReceived Event with the server's response message</para>
        /// </summary>
        /// <param name="message">The message.</param>
        /// <remarks></remarks>
        public void Send(String message) {
            if (!Connected) {
                Debug.WriteLine("Sender.Write - client not connected!", "Warning");
                return;
            }
            var sendThrd = new Thread(msg => sender(msg));
            sendThrd.Start(message);
        }

        #endregion


        #region Private

        void sender(object msg) {
            string message = msg as String;
            if (message == null)
                return;
            try {
                NetworkStream netStrm = _client.GetStream();
                netStrm.WriteTimeout = 10000; // ms
                // Translate the passed message into ASCII and store it as a Byte array.
                Byte[] data = this.Encoding.GetBytes(message);
                // Send the message to the connected TcpServer.
                //netStrm.CanWrite
                netStrm.Write(data, 0, data.Length);
                Debug.WriteLine("Sent: " + message);
                var messageSent = MessageSent;
                if (messageSent != null)
                    messageSent(this, null);
            }
            catch (Exception e) {
                Debug.WriteLine("Send(msg)Thread Error - " + e.Message, "Warning");
                this.Close(); // bei Exc auf jeden Fall close, sonst...
            }
            finally {
                if (CloseAfterSent)
                    this.Close(); //... wenn gewünscht
            }
        }


        void senderOld(object msg) {
            string message = msg as String;
            if (message == null)
                return;
            try {
                NetworkStream netStrm = _client.GetStream();
                // Translate the passed message into ASCII and store it as a Byte array.
                Byte[] data = this.Encoding.GetBytes(message);
                // Send the message to the connected TcpServer.
                netStrm.Write(data, 0, data.Length);
                Debug.WriteLine("Sent: " + message);
                var messageSent = MessageSent;
                if (messageSent != null)
                    messageSent(this, null);

                // Buffer to store the response bytes.
                data = new Byte[_bufferSize];
                // String to store the response ASCII representation.
                String responseData = "";
                do {
                    int bytes = netStrm.Read(data, 0, data.Length);
                    responseData += Encoding.GetString(data, 0, bytes);
                } while (netStrm.DataAvailable);
                var responseReceived = this.ResponseReceived;
                if (responseReceived != null) {
                    responseReceived(this, new ResponseReceivedArg { Response = responseData });
                }
            }
            catch (Exception e) {
                Debug.WriteLine("Send(msg)Thread Error - " + e.Message, "Warning");
                this.Close(); // bei Exc auf jeden Fall close, sonst...
            }
            finally {
                if (CloseAfterSent)
                    this.Close(); //... wenn gewünscht
            }
        }


        void reader() {
            while (_doRead) {
                try {
                    NetworkStream netStrm = _client.GetStream();
                    netStrm.ReadTimeout = 3000; // ms


                    // Buffer to store the response bytes.
                    byte[] data = new Byte[_bufferSize];
                    // String to store the response ASCII representation.
                    String responseData = "";
                    do {
                        int bytes = netStrm.Read(data, 0, data.Length);
                        responseData += Encoding.GetString(data, 0, bytes);
                    } while (netStrm.DataAvailable);
                    var responseReceived = this.ResponseReceived;
                    if (responseReceived != null) {
                        responseReceived(this, new ResponseReceivedArg { Response = responseData });
                    }

                }
                catch (Exception e) {
                    //Debug.WriteLine(String.Format("Send(msg)Thread on {1}:{2} Error: {0}", e.Message, IP, Port), "Warning");
                    //this.Close(); // bei Exc auf jeden Fall close, sonst...
                }
                finally {
                    //if (CloseAfterSent)
                    //    this.Close(); //... wenn gewünscht
                }
            }
        }

        #endregion

    }
}
